* The checkbox for staying in HTTPS displayed on the login form when $wgSecureLogin is
enabled has been removed. Instead, whether the user stays in HTTPS will be determined
based on the user's preferences, and whether they came from HTTPS or not.
+* $wgRC2UDPAddress, $wgRC2UDPInterwikiPrefix, $wgRC2UDPOmitBots, $wgRC2UDPPort,
+ and $wgRC2UDPPrefix configuration options have been deprecated in favor of a
+ $wgRCFeeds configuration array. $wgRCFeeds makes both the format and
+ destination of recent change notifications customizable, and allows for
+ multiple destinations to be specified.
=== New features in 1.22 ===
* (bug 44525) mediawiki.jqueryMsg can now parse (whitelisted) HTML elements and attributes.
have been deprecated in favour of using mw.hook.
* The 'showjumplinks' user preference has been removed, jump links are now
always included.
+* Methods RecentChange::notifyRC2UDP, RecentChange::sendToUDP, and
+ RecentChange::cleanupForIRC have been deprecated, as it is now the
+ responsibility of classes implementing the RCFeedFormatter and RCFeedEngine
+ interfaces to implement the formatting and delivery for recent change
+ notifications.
== Compatibility ==
'ProfilerStub' => 'includes/profiler/ProfilerStub.php',
'ProfileSection' => 'includes/profiler/Profiler.php',
+ # includes/rcfeed
+ 'RCFeedEngine' => 'includes/rcfeed/RCFeedEngine.php',
+ 'UDPRCFeedEngine' => 'includes/rcfeed/UDPRCFeedEngine.php',
+ 'RCFeedFormatter' => 'includes/rcfeed/RCFeedFormatter.php',
+ 'IRCColourfulRCFeedFormatter' => 'includes/rcfeed/IRCColourfulRCFeedFormatter.php',
+ 'JSONRCFeedFormatter' => 'includes/rcfeed/JSONRCFeedFormatter.php',
+
# includes/resourceloader
'ResourceLoader' => 'includes/resourceloader/ResourceLoader.php',
'ResourceLoaderContext' => 'includes/resourceloader/ResourceLoaderContext.php',
/**
* Send recent changes updates via UDP. The updates will be formatted for IRC.
* Set this to the IP address of the receiver.
+ *
+ * @deprecated since 1.22, use $wgRCFeeds
*/
$wgRC2UDPAddress = false;
/**
* Port number for RC updates
+ *
+ * @deprecated since 1.22, use $wgRCFeeds
*/
$wgRC2UDPPort = false;
* This can be used to identify the wiki. A script is available called
* mxircecho.py which listens on a UDP port, and uses a prefix ending in a
* tab to identify the IRC channel to send the log line to.
+ *
+ * @deprecated since 1.22, use $wgRCFeeds
*/
$wgRC2UDPPrefix = '';
/**
* If this is set to true, $wgLocalInterwiki will be prepended to links in the
* IRC feed. If this is set to a string, that string will be used as the prefix.
+ *
+ * @deprecated since 1.22, use $wgRCFeeds
*/
$wgRC2UDPInterwikiPrefix = false;
/**
* Set to true to omit "bot" edits (by users with the bot permission) from the
* UDP feed.
+ *
+ * @deprecated since 1.22, use $wgRCFeeds
*/
$wgRC2UDPOmitBots = false;
+/**
+ * Destinations to which notifications about recent changes
+ * should be sent.
+ *
+ * As of MediaWiki 1.22, the only supported 'engine' parameter option in core
+ * is 'UDPRCFeedEngine', which is used to send recent changes over UDP to the
+ * specified server.
+ * The common options are:
+ * * 'uri' -- the address to which the notices are to be sent.
+ * * 'formatter' -- the class name (implementing RCFeedFormatter) which will
+ * produce the text to send.
+ * * 'omit_bots' -- whether the bot edits should be in the feed
+ * The IRC-specific options are:
+ * * 'add_interwiki_prefix' -- whether the titles should be prefixed with
+ * $wgLocalInterwiki.
+ * The JSON-specific options are:
+ * * 'channel' -- if set, the 'channel' parameter is also set in JSON values.
+ *
+ * To ensure backwards-compatability, whenever $wgRC2UDPAddress is set, a
+ * 'default' feed will be created reusing the deprecated $wgRC2UDP* variables.
+ *
+ * @example $wgRCFeeds['example'] = array(
+ * 'formatter' => 'JSONRCFeedFormatter',
+ * 'uri' => "udp://localhost:1336",
+ * 'add_interwiki_prefix' => false,
+ * 'omit_bots' => true,
+ * );
+ * @example $wgRCFeeds['exampleirc'] = array(
+ * 'formatter' => 'IRCColourfulRCFeedFormatter',
+ * 'uri' => "udp://localhost:1338",
+ * 'add_interwiki_prefix' => false,
+ * 'omit_bots' => true,
+ * );
+ * @since 1.22
+ */
+$wgRCFeeds = array();
+
+/**
+ * Used by RecentChange::getStreamEngine to find the correct engine to use for a given URI protocol.
+ * Keys are scheme names, values are names of engine classes.
+ */
+$wgStreamLoggers = array(
+ 'udp' => 'UDPRCFeedEngine',
+);
+
/**
* Enable user search in Special:Newpages
* This is really a temporary hack around an index install bug on some Wikipedias.
# Notify external application via UDP
if ( !$noudp ) {
- $this->notifyRC2UDP();
+ $this->notifyRCFeeds();
}
# E-mail notifications
}
}
+ /**
+ * @deprecated since 1.22, use notifyRCFeeds instead.
+ */
public function notifyRC2UDP() {
- global $wgRC2UDPAddress, $wgRC2UDPOmitBots;
- # Notify external application via UDP
- # Omit RC_EXTERNAL changes: bots and tools can get these edits from the feed of the external wiki
- if ( $wgRC2UDPAddress && $this->mAttribs['rc_type'] != RC_EXTERNAL &&
- ( !$this->mAttribs['rc_bot'] || !$wgRC2UDPOmitBots ) ) {
- self::sendToUDP( $this->getIRCLine() );
- }
+ wfDeprecated( __METHOD__, '1.22' );
+ $this->notifyRCFeeds();
}
/**
* Send some text to UDP.
- * @see RecentChange::cleanupForIRC
- * @param string $line text to send
- * @param string $address defaults to $wgRC2UDPAddress.
- * @param string $prefix defaults to $wgRC2UDPPrefix.
- * @param int $port defaults to $wgRC2UDPPort. (Since 1.17)
- * @return Boolean: success
+ * @deprecated since 1.22
*/
public static function sendToUDP( $line, $address = '', $prefix = '', $port = '' ) {
- global $wgRC2UDPAddress, $wgRC2UDPPrefix, $wgRC2UDPPort;
- # Assume default for standard RC case
- $address = $address ? $address : $wgRC2UDPAddress;
- $prefix = $prefix ? $prefix : $wgRC2UDPPrefix;
- $port = $port ? $port : $wgRC2UDPPort;
- # Notify external application via UDP
- if ( $address ) {
- $conn = socket_create( AF_INET, SOCK_DGRAM, SOL_UDP );
- if ( $conn ) {
- $line = $prefix . $line;
- wfDebug( __METHOD__ . ": sending UDP line: $line\n" );
- socket_sendto( $conn, $line, strlen( $line ), 0, $address, $port );
- socket_close( $conn );
- return true;
+ global $wgRC2UDPPrefix, $wgRC2UDPInterwikiPrefix;
+ wfDeprecated( __METHOD__, '1.22' );
+
+ $engine = new UDPRCFeedEngine();
+ $feed = array(
+ 'uri' => "udp://$address:$port/$wgRC2UDPPrefix",
+ 'formatter' => 'IRCColourfulRCFeedFormatter',
+ 'add_interwiki_prefix' => $wgRC2UDPInterwikiPrefix,
+ );
+
+ return $engine->send( $feed, $line );
+ }
+
+ /**
+ * Notify all the feeds about the change.
+ */
+ public function notifyRCFeeds() {
+ global $wgRCFeeds;
+
+ foreach ( $wgRCFeeds as $feed ) {
+ $engine = self::getStreamEngine( $feed['uri'] );
+
+ if ( isset( $this->mExtras['actionCommentIRC'] ) ) {
+ $actionComment = $this->mExtras['actionCommentIRC'];
} else {
- wfDebug( __METHOD__ . ": failed to create UDP socket\n" );
+ $actionComment = null;
}
+
+ $omitBots = isset( $feed['omit_bots'] ) ? $feed['omit_bots'] : false;
+
+ if (
+ ( $omitBots && $this->mAttribs['rc_bot'] ) ||
+ $this->mAttribs['rc_type'] == RC_EXTERNAL
+ ) {
+ continue;
+ }
+
+ $formatter = new $feed['formatter']();
+ $line = $formatter->getLine( $feed, $this, $actionComment );
+
+ $engine->send( $feed, $line );
}
- return false;
}
/**
- * Remove newlines, carriage returns and decode html entities
- * @param $text String
- * @return String
+ * Gets the stream engine object for a given URI from $wgStreamLoggers
+ *
+ * @param $uri string URI to get the engine object for
+ * @return object The engine object
+ */
+ private static function getStreamEngine( $uri ) {
+ global $wgStreamLoggers;
+
+ $scheme = parse_url( $uri, PHP_URL_SCHEME );
+ if ( !$scheme ) {
+ throw new MWException( __FUNCTION__ . ": Invalid stream logger URI: '$uri'" );
+ }
+
+ if ( !isset( $wgStreamLoggers[$scheme] ) ) {
+ throw new MWException( __FUNCTION__ . ": Unknown stream logger URI scheme: $scheme" );
+ }
+
+ return new $wgStreamLoggers[$scheme];
+ }
+
+ /**
+ * @deprecated since 1.22, moved to IRCColourfulRCFeedFormatter
*/
public static function cleanupForIRC( $text ) {
- return Sanitizer::decodeCharReferences( str_replace( array( "\n", "\r" ), array( " ", "" ), $text ) );
+ wfDeprecated( __METHOD__, '1.22' );
+ return IRCColourfulRCFeedFormatter::cleanupForIRC( $text );
}
/**
return $trail;
}
- /**
- * @return string
- */
- public function getIRCLine() {
- global $wgUseRCPatrol, $wgUseNPPatrol, $wgRC2UDPInterwikiPrefix, $wgLocalInterwiki,
- $wgCanonicalServer, $wgScript;
-
- if ( $this->mAttribs['rc_type'] == RC_LOG ) {
- // Don't use SpecialPage::getTitleFor, backwards compatibility with
- // IRC API which expects "Log".
- $titleObj = Title::newFromText( 'Log/' . $this->mAttribs['rc_log_type'], NS_SPECIAL );
- } else {
- $titleObj =& $this->getTitle();
- }
- $title = $titleObj->getPrefixedText();
- $title = self::cleanupForIRC( $title );
-
- if ( $this->mAttribs['rc_type'] == RC_LOG ) {
- $url = '';
- } else {
- $url = $wgCanonicalServer . $wgScript;
- if ( $this->mAttribs['rc_type'] == RC_NEW ) {
- $query = '?oldid=' . $this->mAttribs['rc_this_oldid'];
- } else {
- $query = '?diff=' . $this->mAttribs['rc_this_oldid'] . '&oldid=' . $this->mAttribs['rc_last_oldid'];
- }
- if ( $wgUseRCPatrol || ( $this->mAttribs['rc_type'] == RC_NEW && $wgUseNPPatrol ) ) {
- $query .= '&rcid=' . $this->mAttribs['rc_id'];
- }
- // HACK: We need this hook for WMF's secure server setup
- wfRunHooks( 'IRCLineURL', array( &$url, &$query ) );
- $url .= $query;
- }
-
- if ( $this->mAttribs['rc_old_len'] !== null && $this->mAttribs['rc_new_len'] !== null ) {
- $szdiff = $this->mAttribs['rc_new_len'] - $this->mAttribs['rc_old_len'];
- if ( $szdiff < -500 ) {
- $szdiff = "\002$szdiff\002";
- } elseif ( $szdiff >= 0 ) {
- $szdiff = '+' . $szdiff;
- }
- // @todo i18n with parentheses in content language?
- $szdiff = '(' . $szdiff . ')';
- } else {
- $szdiff = '';
- }
-
- $user = self::cleanupForIRC( $this->mAttribs['rc_user_text'] );
-
- if ( $this->mAttribs['rc_type'] == RC_LOG ) {
- $targetText = $this->getTitle()->getPrefixedText();
- $comment = self::cleanupForIRC( str_replace( "[[$targetText]]", "[[\00302$targetText\00310]]", $this->mExtra['actionCommentIRC'] ) );
- $flag = $this->mAttribs['rc_log_action'];
- } else {
- $comment = self::cleanupForIRC( $this->mAttribs['rc_comment'] );
- $flag = '';
- if ( !$this->mAttribs['rc_patrolled'] && ( $wgUseRCPatrol || $this->mAttribs['rc_type'] == RC_NEW && $wgUseNPPatrol ) ) {
- $flag .= '!';
- }
- $flag .= ( $this->mAttribs['rc_type'] == RC_NEW ? "N" : "" ) . ( $this->mAttribs['rc_minor'] ? "M" : "" ) . ( $this->mAttribs['rc_bot'] ? "B" : "" );
- }
-
- if ( $wgRC2UDPInterwikiPrefix === true && $wgLocalInterwiki !== false ) {
- $prefix = $wgLocalInterwiki;
- } elseif ( $wgRC2UDPInterwikiPrefix ) {
- $prefix = $wgRC2UDPInterwikiPrefix;
- } else {
- $prefix = false;
- }
- if ( $prefix !== false ) {
- $titleString = "\00314[[\00303$prefix:\00307$title\00314]]";
- } else {
- $titleString = "\00314[[\00307$title\00314]]";
- }
-
- # see http://www.irssi.org/documentation/formats for some colour codes. prefix is \003,
- # no colour (\003) switches back to the term default
- $fullString = "$titleString\0034 $flag\00310 " .
- "\00302$url\003 \0035*\003 \00303$user\003 \0035*\003 $szdiff \00310$comment\003\n";
-
- return $fullString;
- }
-
/**
* Returns the change size (HTML).
* The lengths can be given optionally.
$wgCookieSecure = ( WebRequest::detectProtocol() === 'https' );
}
+if ( $wgRC2UDPAddress ) {
+ $wgRCFeeds['default'] = array(
+ 'formatter' => 'IRCColourfulRCFeedFormatter',
+ 'uri' => "udp://$wgRC2UDPAddress:$wgRC2UDPPort/$wgRC2UDPPrefix",
+ 'add_interwiki_prefix' => &$wgRC2UDPInterwikiPrefix,
+ 'omit_bots' => &$wgRC2UDPOmitBots,
+ );
+}
+
// Disable MWDebug for command line mode, this prevents MWDebug from eating up
// all the memory from logging SQL queries on maintenance scripts
global $wgCommandLineMode;
}
if ( $to === 'udp' || $to === 'rcandudp' ) {
- $rc->notifyRC2UDP();
+ $rc->notifyRCFeeds();
}
}
--- /dev/null
+<?php
+class IRCColourfulRCFeedFormatter implements RCFeedFormatter {
+ /**
+ * Generates a colourful notification intended for humans on IRC.
+ * @see RCFeedFormatter::getLine
+ */
+ public function getLine( array $feed, RecentChange $rc, $actionComment ) {
+ global $wgUseRCPatrol, $wgUseNPPatrol, $wgLocalInterwiki,
+ $wgCanonicalServer, $wgScript;
+ $attribs = $rc->getAttributes();
+ if ( $attribs['rc_type'] == RC_LOG ) {
+ // Don't use SpecialPage::getTitleFor, backwards compatibility with
+ // IRC API which expects "Log".
+ $titleObj = Title::newFromText( 'Log/' . $attribs['rc_log_type'], NS_SPECIAL );
+ } else {
+ $titleObj =& $rc->getTitle();
+ }
+ $title = $titleObj->getPrefixedText();
+ $title = self::cleanupForIRC( $title );
+
+ if ( $attribs['rc_type'] == RC_LOG ) {
+ $url = '';
+ } else {
+ $url = $wgCanonicalServer . $wgScript;
+ if ( $attribs['rc_type'] == RC_NEW ) {
+ $query = '?oldid=' . $attribs['rc_this_oldid'];
+ } else {
+ $query = '?diff=' . $attribs['rc_this_oldid'] . '&oldid=' . $attribs['rc_last_oldid'];
+ }
+ if ( $wgUseRCPatrol || ( $attribs['rc_type'] == RC_NEW && $wgUseNPPatrol ) ) {
+ $query .= '&rcid=' . $attribs['rc_id'];
+ }
+ // HACK: We need this hook for WMF's secure server setup
+ wfRunHooks( 'IRCLineURL', array( &$url, &$query ) );
+ $url .= $query;
+ }
+
+ if ( $attribs['rc_old_len'] !== null && $attribs['rc_new_len'] !== null ) {
+ $szdiff = $attribs['rc_new_len'] - $attribs['rc_old_len'];
+ if ( $szdiff < -500 ) {
+ $szdiff = "\002$szdiff\002";
+ } elseif ( $szdiff >= 0 ) {
+ $szdiff = '+' . $szdiff;
+ }
+ // @todo i18n with parentheses in content language?
+ $szdiff = '(' . $szdiff . ')';
+ } else {
+ $szdiff = '';
+ }
+
+ $user = self::cleanupForIRC( $attribs['rc_user_text'] );
+
+ if ( $attribs['rc_type'] == RC_LOG ) {
+ $targetText = $rc->getTitle()->getPrefixedText();
+ $comment = self::cleanupForIRC( str_replace( "[[$targetText]]", "[[\00302$targetText\00310]]", $actionComment ) );
+ $flag = $attribs['rc_log_action'];
+ } else {
+ $comment = self::cleanupForIRC( $attribs['rc_comment'] );
+ $flag = '';
+ if ( !$attribs['rc_patrolled'] && ( $wgUseRCPatrol || $attribs['rc_type'] == RC_NEW && $wgUseNPPatrol ) ) {
+ $flag .= '!';
+ }
+ $flag .= ( $attribs['rc_type'] == RC_NEW ? "N" : "" ) . ( $attribs['rc_minor'] ? "M" : "" ) . ( $attribs['rc_bot'] ? "B" : "" );
+ }
+
+ if ( $feed['add_interwiki_prefix'] === true && $wgLocalInterwiki !== false ) {
+ $prefix = $wgLocalInterwiki;
+ } elseif ( $feed['add_interwiki_prefix'] ) {
+ $prefix = $feed['add_interwiki_prefix'];
+ } else {
+ $prefix = false;
+ }
+ if ( $prefix !== false ) {
+ $titleString = "\00314[[\00303$prefix:\00307$title\00314]]";
+ } else {
+ $titleString = "\00314[[\00307$title\00314]]";
+ }
+
+ # see http://www.irssi.org/documentation/formats for some colour codes. prefix is \003,
+ # no colour (\003) switches back to the term default
+ $fullString = "$titleString\0034 $flag\00310 " .
+ "\00302$url\003 \0035*\003 \00303$user\003 \0035*\003 $szdiff \00310$comment\003\n";
+
+ return $fullString;
+ }
+
+ /**
+ * Remove newlines, carriage returns and decode html entites
+ * @param string $text
+ * @return string
+ */
+ public static function cleanupForIRC( $text ) {
+ return Sanitizer::decodeCharReferences( str_replace(
+ array( "\n", "\r" ),
+ array( " ", "" ),
+ $text
+ ) );
+ }
+}
--- /dev/null
+<?php
+class JSONRCFeedFormatter implements RCFeedFormatter {
+ /**
+ * Generates a notification that can be easily interpreted by a machine.
+ * @see RCFeedFormatter::getLine
+ */
+ public function getLine( array $feed, RecentChange $rc, $actionComment ) {
+ global $wgCanonicalServer, $wgScriptPath, $wgArticlePath, $wgDBname;
+ $attrib = $rc->getAttributes();
+
+ $packet = array(
+ // Usually, RC ID is exposed only for patrolling purposes,
+ // but there is no real reason not to expose it in other cases,
+ // and I can see how this may be potentially useful for clients.
+ 'id' => $attrib['rc_id'],
+ 'type' => $attrib['rc_type'],
+ 'namespace' => $rc->getTitle()->getNamespace(),
+ 'title' => $rc->getTitle()->getPrefixedText(),
+ 'comment' => $attrib['rc_comment'],
+ 'timestamp' => (int)wfTimestamp( TS_UNIX, $attrib['rc_timestamp'] ),
+ 'user' => $attrib['rc_user_text'],
+ 'bot' => (bool)$attrib['rc_bot'],
+ );
+
+ if ( isset( $feed['channel'] ) ) {
+ $packet['channel'] = $feed['channel'];
+ }
+
+ $type = $attrib['rc_type'];
+ if ( $type == RC_EDIT || $type == RC_NEW ) {
+ global $wgUseRCPatrol, $wgUseNPPatrol;
+
+ $packet['minor'] = $attrib['rc_minor'];
+ if ( $wgUseRCPatrol || ( $type == RC_NEW && $wgUseNPPatrol ) ) {
+ $packet['patrolled'] = $attrib['rc_patrolled'];
+ }
+ }
+
+ switch ( $type ) {
+ case RC_EDIT:
+ $packet['length'] = array( 'old' => $attrib['rc_old_len'], 'new' => $attrib['rc_new_len'] );
+ $packet['revision'] = array( 'old' => $attrib['rc_last_oldid'], 'new' => $attrib['rc_this_oldid'] );
+ break;
+
+ case RC_NEW:
+ $packet['length'] = array( 'old' => NULL, 'new' => $attrib['rc_new_len'] );
+ $packet['revision'] = array( 'old' => NULL, 'new' => $attrib['rc_this_oldid'] );
+ break;
+
+ case RC_LOG:
+ $packet['log_type'] = $attrib['rc_log_type'];
+ $packet['log_action'] = $attrib['rc_log_action'];
+ if ( $attrib['rc_params'] ) {
+ wfSuppressWarnings();
+ $params = unserialize( $attrib['rc_params'] );
+ wfRestoreWarnings();
+ if (
+ // If it's an actual serialised false...
+ $attrib['rc_params'] == serialize( false ) ||
+ // Or if we did not get false back when trying to unserialise
+ $params !== false
+ ) {
+ // From ApiQueryLogEvents::addLogParams
+ $logParams = array();
+ // Keys like "4::paramname" can't be used for output so we change them to "paramname"
+ foreach ( $params as $key => $value ) {
+ if ( strpos( $key, ':' ) === false ) {
+ $logParams[$key] = $value;
+ continue;
+ }
+ $logParam = explode( ':', $key, 3 );
+ $logParams[$logParam[2]] = $value;
+ }
+ $packet['log_params'] = $logParams;
+ } else {
+ $packet['log_params'] = explode( "\n", $attrib['rc_params'] );
+ }
+ }
+ $packet['log_action_comment'] = $actionComment;
+ break;
+ }
+
+ $packet['server_url'] = $wgCanonicalServer;
+ $packet['server_script_path'] = $wgScriptPath ?: '/';
+ $packet['wiki'] = $wgDBname;
+
+ return FormatJson::encode( $packet );
+ }
+}
--- /dev/null
+<?php
+interface RCFeedEngine {
+ /**
+ * Sends some text to the specified live feed.
+ *
+ * @see RecentChange::cleanupForIRC
+ * @param array $feed The feed, as configured in an associative array.
+ * @param string $line The text to send.
+ * @return boolean success
+ */
+ public function send( array $feed, $line );
+}
--- /dev/null
+<?php
+interface RCFeedFormatter {
+ /**
+ * Formats the line for the live feed.
+ *
+ * @param array $feed The feed, as configured in an associative array.
+ * @param RecentChange $rc The RecentChange object showing what sort
+ * of event has taken place.
+ * @param string|null $actionComment
+ * @return string The text to send.
+ */
+ public function getLine( array $feed, RecentChange $rc, $actionComment );
+}
--- /dev/null
+<?php
+class UDPRCFeedEngine implements RCFeedEngine {
+ /**
+ * Sends the notification to the specified host in a UDP packet.
+ * @see RCFeedEngine::send
+ */
+ public function send( array $feed, $line ) {
+ wfErrorLog( $line, $feed['uri'] );
+ }
+}
$formatter = LogFormatter::newFromEntry( $logEntry );
$formatter->setContext( $this->context );
- // Apply the same transformation as done in RecentChange::getIRCLine for rc_comment
- $ircRcComment = RecentChange::cleanupForIRC( $formatter->getIRCActionComment() );
+ // Apply the same transformation as done in IRCColourfulRCFeedFormatter::getLine for rc_comment
+ $ircRcComment = IRCColourfulRCFeedFormatter::cleanupForIRC( $formatter->getIRCActionComment() );
$this->assertEquals(
$expected,